import sys,os
import time
import math,random
import csv,codecs
import numpy as np
import pandas as pd

from django.shortcuts import render
from django.views.generic.base import View
from apscheduler.schedulers.blocking import BlockingScheduler
from django.http import HttpResponse
from django.utils.decorators import method_decorator

from users.models import UserProfile
from movieinfo.models import Movie,MovieParticulars
from .models import UserRecommend, UserRatings,UserFavorite
from operator import itemgetter


# 基于项目的协同过滤推荐算法实现
class ItemBasedCF():
    # 初始化参数
    def __init__(self):
        # 找到相似的20部电影，为目标用户推荐10部电影
        self.n_sim_movie = 20
        self.n_rec_movie = 10

        # 将数据集划分为训练集和测试集
        self.trainSet = {}
        self.testSet = {}

        # 用户相似度矩阵
        self.movie_sim_matrix = {}
        self.movie_popular = {}
        self.movie_count = 0

    # 读文件得到“用户-电影”数据
    def get_dataset(self, filename, pivot=0.875):
        trainSet_len = 0
        testSet_len = 0
        for line in self.load_file(filename):
            user, movie, rating = line.split(',')
            if (random.random() < pivot):
                self.trainSet.setdefault(user, {})  # 相当于trainSet.get(user)，若该键不存在，则设trainSet[user] = {}，典中典

                # 键中键：形如{'1': {'1287': '2.0', '1953': '4.0', '2105': '4.0'}, '2': {'10': '4.0', '62': '3.0'}}
                # 用户1看了id为1287的电影，打分2.0
                self.trainSet[user][movie] = rating
                trainSet_len += 1
            else:
                self.testSet.setdefault(user, {})
                self.testSet[user][movie] = rating
                testSet_len += 1
        print('分割训练集和测试集成功!')
        print('训练集 = %s' % trainSet_len)
        print('测试集t = %s' % testSet_len)

    # 读文件，返回文件的每一行
    def load_file(self, filename):
        fp = open(filename, 'r', encoding='UTF-8')
        for i, line in enumerate(fp):
            yield line.strip('\r\n')
        fp.close()
        print('读取 %s 成功!' % filename, file=sys.stderr)


    # 计算电影之间的相似度
    def calc_movie_sim(self):
        t1 = time.time()
        print('计算电影数量和受欢迎程度……', file=sys.stderr)
        for user, movies in self.trainSet.items():  # 循环取出一个用户和他看过的电影
            for movie in movies:
                if movie not in self.movie_popular:
                    self.movie_popular[movie] = 0
                self.movie_popular[movie] += 1  # 统计每部电影共被看过的次数

        self.movie_count = len(self.movie_popular)  # 得到电影总数
        print("电影总数量 = %d" % self.movie_count)

        for user, movies in self.trainSet.items():  # 得到矩阵C，C[i][j]表示同时喜欢电影i和j的用户数
            for m1 in movies:
                for m2 in movies:
                    if m1 == m2:
                        continue
                    self.movie_sim_matrix.setdefault(m1, {})
                    self.movie_sim_matrix[m1].setdefault(m2, 0)
                    # self.movie_sim_matrix[m1][m2] += 1  #同时喜欢电影m1和m2的用户+1    21.75  10.5   16.67
                    self.movie_sim_matrix[m1][m2] += 1 / math.log(
                        1 + len(movies))  # ItemCF-IUF改进，惩罚了活跃用户 22.00 10.65 14.98
        print("建立共同评价用户矩阵成功")

        # 计算电影之间的相似性
        print("计算电影相似矩阵…")
        for m1, related_movies in self.movie_sim_matrix.items():  # 电影m1，及m1这行对应的电影们
            for m2, count in related_movies.items():  # 电影m2 及 同时看了m1和m2的用户数
                # 注意0向量的处理，即某电影的用户数为0
                if self.movie_popular[m1] == 0 or self.movie_popular[m2] == 0:
                    self.movie_sim_matrix[m1][m2] = 0
                else:
                    # 计算出电影m1和m2的相似度
                    self.movie_sim_matrix[m1][m2] = count / math.sqrt(self.movie_popular[m1] * self.movie_popular[m2])
        print('计算电影相似矩阵成功!')  # 建立共同评价用户矩阵成功

        # 添加归一化    precisioin=0.2177	recall=0.1055	coverage=0.1497
        maxDict = {}
        max = 0
        for m1, related_movies in self.movie_sim_matrix.items():
            for m2, _ in related_movies.items():
                if self.movie_sim_matrix[m1][m2] > max:
                    max = self.movie_sim_matrix[m1][m2]

        for m1, related_movies in self.movie_sim_matrix.items():  # 归一化
            for m2, _ in related_movies.items():
                # self.movie_sim_matrix[m1][m2] = self.movie_sim_matrix[m1][m2] / maxDict[m2]
                self.movie_sim_matrix[m1][m2] = self.movie_sim_matrix[m1][m2] / max
        print('标准化已经完成!')  # 归一化完成
        t2 = time.time()
        t = int(round(t2 - t1))
        print('计算相似度矩阵时间: %d 秒' % t)


    # 针对目标用户U，找到K部相似的电影，并推荐其N部电影
    def recommend(self, user):
        K = self.n_sim_movie  # 找到相似的20部电影
        N = self.n_rec_movie  # 为用户推荐10部
        rank = {}
        watched_movies = self.trainSet[user]  # 该用户看过的电影

        for movie, rating in watched_movies.items():  # 遍历用户看过的电影及其评分
            # 找到与movie最相似的K部电影,遍历电影及与movie相似度
            for related_movie, similarity_factor in sorted(self.movie_sim_matrix[movie].items(), key=itemgetter(1), reverse=True)[:K]:
                if related_movie in watched_movies:  # 如果用户已经看过了，不推荐了
                    continue
                rank.setdefault(related_movie, 0)
                rank[related_movie] += similarity_factor * float(rating)  # 计算用户对该电影的兴趣
        rank_ = sorted(rank.items(), key=itemgetter(1), reverse=True)[:N]
        matrix=[]
        for key, value in rank_:
            matrix.append(key)
        # 返回用户最感兴趣的N部电影
        return matrix
        # return sorted(rank.items(), key=itemgetter(1), reverse=True)[:N]

class RecommendView(View):
    # 完成推荐
    def GenerateRecommendTable(self,request):
        UserRecommend.objects.all().delete()       #数据库中的推荐表删除
        RecommendView.AdditionalData(self)          # 从数据库中获取用户评分数据添加到csv文件
        rating_file = os.path.join('static', 'ratings.csv')     #读取
        itemCF = ItemBasedCF()  # 实例化
        itemCF.get_dataset(rating_file)  # 读取文件
        itemCF.calc_movie_sim()  # 计算相似度矩阵
        i = UserProfile.objects.all().count()  # 用户数量
        for user_id in UserProfile.objects.all():  # 遍历
            if UserRatings.objects.filter(user=user_id).count()!= 0:  # 判断是否进行评分
                matrix = str(itemCF.recommend(str(user_id.id)))  # 进行推荐
                UserRecommend.objects.create(movie_id=matrix, user=user_id)  # 存储
            else:
                movie_res = Movie.objects.all().order_by('-click_nums')[:10]
                matrix = []
                for movie_re in movie_res:
                    matrix.append(str(movie_re.id))
                UserRecommend.objects.create(movie_id=str(matrix), user=user_id)  # 存储推荐


    # csv文件操作
    def AdditionalData(self):
        filename = os.path.join('static', 'ratings.csv')
        filedate = pd.read_csv(filename)
        with codecs.open(filename='ratings.csv', mode='a', encoding='utf-8')as f:
            write=csv.writer(f, dialect='excel')
            #  删除原来的评分数据
            results=UserRatings.objects.all()
            for result in results:
                filedate = filedate[~filedate['userId'].isin([str(result.user.id)])]
            # 增加用户的评分数据
            results=UserRatings.objects.all().values_list('user','movie','ratings')
            for result in results:
                write.writerow(result[:-1])

    def recommend(self,request):
        """把推荐数据返回给前端"""
        # 利用apscheduler，完成阻塞任务,每天4：00定时刷新用户推荐表
        #scheduler = BlockingScheduler()
        #scheduler.add_job(RecommendView.GenerateRecommendTable, 'cron', hour='4',minute='0',)
        #try:
            #scheduler.start()
        RecommendView.GenerateRecommendTable(self,request)
        #finally:
        user_recommend = UserRecommend.objects.get(user=request.user)
        user_recommend = str(user_recommend.movie_id)
        # 获取的数据库的数据是否要转换格式 , 这里不需要，配置了models的
        movie_re=user_recommend.replace("'","").strip("[]").strip().split(',')
        matrix=[]
        # filename = os.path.join('static', 'movies.csv')
        # filedate = pd.read_csv(filename)
        for line in movie_re:
            movie_re = Movie.objects.get(id=int(line))
            movie_re=MovieParticulars.objects.get(name=movie_re)
            # movie_re=filedate.loc[filedate['movieId']==str(line),'title']
            matrix.append(movie_re)
        return matrix

